Author

Diego Cruz Aguilar

Published

August 11, 2024

Modified

July 17, 2025

Code
from pathlib import Path
Code
import altair as alt
import os
import polars as pl

def plot_metric(df, title, y_col, y_label, image_name=None, sort_order='-y'):
    """Generates a bar chart for a given metric and optionally saves it as SVG."""
    chart = (
        alt.Chart(df)
        .mark_bar()
        .encode(
            x=alt.X("classifier:N", title="Classifier", sort=sort_order),
            y=alt.Y(f"{y_col}:Q", title=y_label),
            color=alt.Color("classifier:N", legend=alt.Legend(title="Classifier")),
            tooltip=["classifier", y_col],
        )
        .properties(title=title, width=600, height=400)
    )

    if image_name:
        try:
            os.makedirs("./images", exist_ok=True)
            chart.save(f"./images/{image_name}.svg")
        except Exception as e:
            print(f"Error saving chart as SVG: {e}")

    return chart


def plot_confusion_matrix(reporter, classifier_name, image_name=None):
    """Generates and displays a confusion matrix for a given classifier, optionally saving as SVG."""
    cm_df = reporter.get_confusion_matrix(classifier_name)

    if cm_df.is_empty():
        print(f"No confusion matrix data for {classifier_name}")
        return None

    base = alt.Chart(cm_df).encode(
        x=alt.X("predicted_class:N", title="Predicted"),
        y=alt.Y("true_class:N", title="Actual"),
    )

    heatmap = base.mark_rect().encode(
        color=alt.Color("count:Q", title="Count", scale=alt.Scale(scheme='viridis')),
        tooltip=["predicted_class", "true_class", "count"],
    )

    text = base.mark_text(align="center", baseline="middle", fontSize=12).encode(
        text=alt.Text("count:Q"),
        color=alt.condition(
            alt.datum.count > cm_df['count'].max() / 2,
            alt.value("white"),
            alt.value("black"),
        ),
    )

    final_chart = (heatmap + text).properties(
        title=f"Confusion Matrix for {classifier_name}", width=400, height=400
    )

    if image_name:
        try:
            os.makedirs("./images", exist_ok=True)
            final_chart.save(f"./images/{image_name}.svg")
        except Exception as e:
            print(f"Error saving chart as SVG: {e}")
    return final_chart

def plot_biometrics_per_class(reporter, classifier_name, metric, image_name=None):
    """
    Generates a dot plot for per-class biometric metrics (EER or AUC)
    with a reference line for the mean.
    """
    df = reporter.get_per_class_biometrics(classifier_name)
    if df.is_empty():
        print(f"No per-class biometric data for {classifier_name}")
        return None

    if metric not in df.columns:
        print(f"Metric '{metric}' not found in per-class data.")
        return None

    # Calculate the mean for the reference line
    mean_val = df[metric].mean()

    sort_order = "ascending" if metric == "eer" else "descending"
    y_label = "EER (Lower is Better)" if metric == "eer" else "AUC (Higher is Better)"
    title = f"{metric.upper()} per Class for {classifier_name}"

    # Create the main dot plot
    points = alt.Chart(df).mark_point(filled=True, size=80).encode(
        x=alt.X("class:N", title="Class/Subject", sort=alt.EncodingSortField(field=metric, op="min", order=sort_order)),
        y=alt.Y(f"{metric}:Q", title=y_label),
        color=alt.Color("class:N", legend=None),
        tooltip=["class", metric],
    )

    # Create the reference line
    rule = alt.Chart(pl.DataFrame({'y': [mean_val]})).mark_rule(color='red', strokeDash=[5,5], size=2).encode(y='y:Q')
    
    final_chart = (points + rule).properties(title=title, width=800, height=400)

    if image_name:
        try:
            os.makedirs("./images", exist_ok=True)
            final_chart.save(f"./images/{image_name}.svg")
        except Exception as e:
            print(f"Error saving chart: {e}")

    return final_chart


def plot_biometric_heatmap(reporter, metric, image_name=None):
    """
    Generates a heatmap to compare a biometric metric (EER or AUC)
    across all classifiers and all classes.
    """
    df = reporter.dataframes.get("per_class_biometrics")
    if df is None or df.is_empty():
        print("No per-class biometric data available to generate a heatmap.")
        return None

    if metric not in df.columns:
        print(f"Metric '{metric}' not found for heatmap.")
        return None

    # For EER, lower is better. For AUC, higher is better.
    # We use a color scheme where green is good.
    color_scheme = 'redyellowgreen'
    reverse_scale = True if metric == 'eer' else False
    title = f"Heatmap of {metric.upper()} per Class and Classifier"

    chart = alt.Chart(df).mark_rect().encode(
        x=alt.X('classifier:N', title='Classifier'),
        y=alt.Y('class:N', title='Class/Subject'),
        color=alt.Color(f'{metric}:Q', 
                        scale=alt.Scale(scheme=color_scheme, reverse=reverse_scale),
                        legend=alt.Legend(title=f"{metric.upper()}")),
        tooltip=['classifier', 'class', metric]
    ).properties(
        title=title,
        width=600,
        height=800
    )

    if image_name:
        try:
            os.makedirs("./images", exist_ok=True)
            chart.save(f"./images/{image_name}.svg")
        except Exception as e:
            print(f"Error saving chart: {e}")

    return chart


def plot_far_frr_curves(reporter, classifier_name, image_name=None):
    """Plots FAR vs. FRR curves for each class for a given classifier."""
    df = reporter.get_far_frr_curves(classifier_name)
    if df.is_empty():
        print(f"No FAR/FRR curve data for {classifier_name}")
        return None

    curves = (
        alt.Chart(df)
        .mark_line()
        .encode(
            x=alt.X("far:Q", title="False Acceptance Rate (FAR)", scale=alt.Scale(type="log", domain=[0.001, 1])),
            y=alt.Y("frr:Q", title="False Rejection Rate (FRR)", scale=alt.Scale(type="log", domain=[0.01, 1])),
            color=alt.Color("true_class:N", legend=alt.Legend(title="Class/Subject")),
            tooltip=["true_class", "far", "frr"],
        )
    )

    line_data = pl.DataFrame({'x': [0.001, 1], 'y': [0.001, 1]})
    ref_line = alt.Chart(line_data).mark_line(strokeDash=[5, 5], color='gray').encode(x='x:Q', y='y:Q')

    final_chart = (curves + ref_line).properties(
        title=f"FAR vs. FRR (DET Curve) for {classifier_name}",
        width=600, height=600
    ).interactive()

    if image_name:
        try:
            os.makedirs("./images", exist_ok=True)
            final_chart.save(f"./images/{image_name}.svg")
        except Exception as e:
            print(f"Error saving chart: {e}")

    return final_chart
Code
import plotly.graph_objects as go
import numpy as np
from plotly.subplots import make_subplots
from dataclasses import dataclass
from utils.preprocess import SignalPreprocessor

signal_preprocessor = SignalPreprocessor()


@dataclass
class PlotSignalOverlapLabels:
    title: str
    title1: str
    title2: str
    label1: str
    label2: str


overlap_labels = PlotSignalOverlapLabels(
    title="Complete Comparison: Overlapping, Original and Preprocessed",
    title1="Original Signal",
    title2="Preprocessed Signal",
    label1="Original",
    label2="Preprocessed",
)


def plot_signal_overlap(
    signal, signal_preprocessed, fs, signal_type, labels=overlap_labels
):
    # Calculate X axis (time in seconds)
    time_axis = np.arange(len(signal)) / fs

    # Create a figure with 3 subplots (3 rows, 1 column)
    signal_name = signal_type.upper()
    fig = make_subplots(
        rows=3,
        cols=1,
        subplot_titles=(
            f"Overlapping Signals ({signal_name})",
            f"{labels.title1} ({signal_name})",
            f"{labels.title2} ({signal_name})",
        ),
        shared_xaxes=True,  # Share X axis
        vertical_spacing=0.05,  # Reduced vertical spacing
    )

    # --- Add plots to each subplot ---
    # 1. Overlapping signals (first subplot)
    fig.add_trace(
        go.Scatter(
            x=time_axis,
            y=signal,
            mode="lines",
            name=labels.label1,
            line=dict(color="blue", width=2),
            opacity=0.7,
            showlegend=True,  # Show in legend
        ),
        row=1,
        col=1,
    )
    fig.add_trace(
        go.Scatter(
            x=time_axis,
            y=signal_preprocessed,
            mode="lines",
            name=labels.label2,
            line=dict(color="red", width=2),
            opacity=0.7,
            showlegend=True,  # Show in legend
        ),
        row=1,
        col=1,
    )

    # 2. Original signal alone (second subplot)
    fig.add_trace(
        go.Scatter(
            x=time_axis,
            y=signal,
            mode="lines",
            name=labels.label1,
            line=dict(color="blue", width=2),
            opacity=1.0,  # Full opacity
            showlegend=False,  # Avoid duplicating legend
        ),
        row=2,
        col=1,
    )

    # 3. Preprocessed signal alone (third subplot)
    fig.add_trace(
        go.Scatter(
            x=time_axis,
            y=signal_preprocessed,
            mode="lines",
            name=labels.label2,
            line=dict(color="red", width=2),
            opacity=1.0,  # Full opacity
            showlegend=False,  # Avoid duplicating legend
        ),
        row=3,
        col=1,
    )

    # --- Layout configuration ---
    fig.update_layout(
        title_text=labels.title,
        xaxis3_title="Time (seconds)",  # Only the last subplot shows X axis
        yaxis_title="Amplitude",
        legend=dict(
            orientation="h",  # Horizontal legend
            yanchor="bottom",
            y=1.02,
            xanchor="right",
            x=1,
            font=dict(size=12),
        ),
        template="plotly_white",
        font=dict(size=12),
        height=800,  # Adjusted height for 3 subplots
    )

    # Add Y axis titles to lower subplots
    fig.update_yaxes(title_text="Amplitude", row=2, col=1)
    fig.update_yaxes(title_text="Amplitude", row=3, col=1)

    # Show plot
    fig.show()
    return signal_preprocessed


@dataclass
class PlotLavels:
    title: str
    name: str


def plot_three_signals(
    original_signal,
    signal1,
    signal2,
    titles: tuple[PlotLavels, PlotLavels, PlotLavels],
    fs: int,
    signal_type,
):
    """
    Plots three signals with overlapping and individual subplots.

    Parameters:
        signal1 (array-like): First signal to plot.
        signal2 (array-like): Second signal to plot.
        signal3 (array-like): Third signal to plot.
        fs (float): Sampling frequency of the signals.
        signal_type (str): Type of signal (e.g., "ppg", "ecg"). Default is "ppg".
    """
    # Calculate X axis (time in seconds)
    time_axis = np.arange(len(original_signal)) / fs

    # Create a figure with 4 subplots (4 rows, 1 column)
    signal_name = signal_type.upper()
    fig = make_subplots(
        rows=4,
        cols=1,
        subplot_titles=(
            f"Overlapping Signals ({signal_name})",
            titles[0].title,
            titles[1].title,
            titles[2].title,
        ),
        shared_xaxes=True,  # Share X axis
        vertical_spacing=0.05,  # Reduced vertical spacing
    )

    # --- Add plots to each subplot ---
    # 1. Overlapping signals (first subplot)
    fig.add_trace(
        go.Scatter(
            x=time_axis,
            y=original_signal,
            mode="lines",
            name=titles[0].name,
            line=dict(color="blue", width=2),
            opacity=0.7,
            showlegend=True,  # Show in legend
        ),
        row=1,
        col=1,
    )
    fig.add_trace(
        go.Scatter(
            x=time_axis,
            y=signal1,
            mode="lines",
            name=titles[1].name,
            line=dict(color="green", width=2),
            opacity=0.7,
            showlegend=True,  # Show in legend
        ),
        row=1,
        col=1,
    )
    fig.add_trace(
        go.Scatter(
            x=time_axis,
            y=signal2,
            mode="lines",
            name=titles[2].name,
            line=dict(color="red", width=2),
            opacity=0.7,
            showlegend=True,  # Show in legend
        ),
        row=1,
        col=1,
    )

    # 2. Signal 1 alone (second subplot)
    fig.add_trace(
        go.Scatter(
            x=time_axis,
            y=original_signal,
            mode="lines",
            name=titles[0].name,
            line=dict(color="blue", width=2),
            opacity=1.0,  # Full opacity
            showlegend=False,  # Avoid duplicating legend
        ),
        row=2,
        col=1,
    )

    # 3. Signal 2 alone (third subplot)
    fig.add_trace(
        go.Scatter(
            x=time_axis,
            y=signal1,
            mode="lines",
            name=titles[1].name,
            line=dict(color="green", width=2),
            opacity=1.0,  # Full opacity
            showlegend=False,  # Avoid duplicating legend
        ),
        row=3,
        col=1,
    )

    # 4. Signal 3 alone (fourth subplot)
    fig.add_trace(
        go.Scatter(
            x=time_axis,
            y=signal2,
            mode="lines",
            name=titles[2].name,
            line=dict(color="red", width=2),
            opacity=1.0,  # Full opacity
            showlegend=False,  # Avoid duplicating legend
        ),
        row=4,
        col=1,
    )

    # --- Layout configuration ---
    fig.update_layout(
        title_text="Complete Comparison: Overlapping and Individual Signals",
        xaxis4_title="Time (seconds)",  # Only the last subplot shows X axis
        yaxis_title="Amplitude",
        legend=dict(
            orientation="h",  # Horizontal legend
            yanchor="bottom",
            y=1.02,
            xanchor="right",
            x=1,
            font=dict(size=12),
        ),
        template="plotly_white",
        font=dict(size=12),
        height=1000,  # Adjusted height for 4 subplots
    )

    # Add Y axis titles to lower subplots
    fig.update_yaxes(title_text="Amplitude", row=2, col=1)
    fig.update_yaxes(title_text="Amplitude", row=3, col=1)
    fig.update_yaxes(title_text="Amplitude", row=4, col=1)

    # Show plot
    fig.show()


def plot_compararative_full_preprocess_vs_segment(
    signal, fs, segment_size_seconds, signal_type
):
    title = signal_type.upper()
    signal_segment_size = fs * segment_size_seconds
    end_segment = signal_segment_size * 2

    signal_preprocessed = signal_preprocessor.preprocess_signal(
        signal, fs, signal_type=signal_type
    )

    # Show full signal
    plot_signal_overlap(
        signal,
        signal_preprocessed,
        fs,
        signal_type,
    )

    plot_signal_overlap(
        signal[signal_segment_size:end_segment],
        signal_preprocessed[signal_segment_size:end_segment],
        fs,
        signal_type,
    )

    signal_orginal_label = PlotLavels(f"{title} original signal", f"{title} original")

    signal_preprocessed_label = PlotLavels(
        f"{title} complete signal segment", f"{title} full preprocessing"
    )

    signal_segment_pereprocessed = signal_preprocessor.preprocess_signal(
        signal[signal_segment_size:end_segment], fs, signal_type=signal_type
    )

    signal_segment_label = PlotLavels(f"{title} individual segment", f"{title} segment")

    plot_three_signals(
        signal[signal_segment_size:end_segment],
        signal_preprocessed[signal_segment_size:end_segment],
        signal_segment_pereprocessed,
        (signal_orginal_label, signal_preprocessed_label, signal_segment_label),
        fs,
        signal_type=signal_type,
    )


def plot_compararative_preprocess_vs_reference(signal_raw, signal_ref, fs, signal_type):
    signal_preprocessed = signal_preprocessor.preprocess_signal(
        signal_raw, fs, signal_type=signal_type
    )

    overlap_ref_labels = PlotSignalOverlapLabels(
        title="Complete Comparison: Overlapping, Preprocessed and Reference",
        title1="Reference signal",
        title2="Preprocessed signal",
        label1="Reference",
        label2="Preprocessed",
    )

    # Show full signal
    plot_signal_overlap(
        signal_ref,
        signal_preprocessed,
        fs,
        signal_type,
        labels=overlap_ref_labels,
    )
Code
import polars as pl
from IPython.display import Markdown, display


def dataframe_to_latex_htmltab(
    caption: str,
    df: pl.DataFrame,
    selected_headers: list[str] | None = None,
    show_code: bool = True,
) -> str:
    """
    Converts a Polars DataFrame to a LaTeX htmltab environment and optionally displays it
    as a syntax-highlighted block in Jupyter (copiable).

    Args:
        caption (str): Caption and label identifier for the table.
        df (pl.DataFrame): The DataFrame to convert.
        selected_headers (list[str], optional): Columns to include.
        show_code (bool): If True, shows the LaTeX as a Markdown code block.

    Returns:
        str: LaTeX code for the table.
    """
    if selected_headers:
        df = df.select(selected_headers)

    header = "                " + "\n                ".join(
        [f"\\HTtd{{{col.replace('_', '\\_')}}}" for col in df.columns]
    )

    body = ""
    for row in df.iter_rows():
        body += "            \\HTtr{\n"
        body += (
            "                "
            + "\n                ".join([f"\\HTtd{{{cell}}}" for cell in row])
            + "\n"
        )
        body += "            }\n"

    latex_table = f"""\\begin{{table}}[H]
    \\centering
    \\caption{{Tabla autogenerada}}
    \\label{{tab:{caption}}}
    \\begin{{htmltab}}
        \\begin{{thead}}
            \\HTtr{{
{header}
            }}
        \\end{{thead}}
        \\begin{{tbody}}
{body}        \\end{{tbody}}
    \\end{{htmltab}}
\\end{{table}}"""

    if show_code:
        # Mostrar como bloque de código copiable en Jupyter
        display(Markdown(f"```latex\n{latex_table}\n```"))

    return latex_table

1 MIMIC-III

Code
import polars as pl
mimic_path = Path("../Datasets/MIMIC")
user1 = pl.read_csv(mimic_path / "data/mimic_perform_af_001_data.csv")
ppg = user1["PPG"].to_list()
ecg = user1["ECG"].to_list()
resp = user1["resp"].to_list()

1.1 Preprocesamiento

1.1.1 Comparativa procesmiento completo vs segmento (PPG)

Code
plot_compararative_full_preprocess_vs_segment(ppg, 125, 30, "ppg")

1.1.2 Comparativa procesmiento completo vs segmento (ECG)

Code
plot_compararative_full_preprocess_vs_segment(ecg, 125, 30, "ecg")

1.1.3 Comparativa procesmiento completo vs segmento (RESP)

Code
plot_compararative_full_preprocess_vs_segment(resp, 125, 30, "resp")

2 ML Results ECG + RESP

Display the all keys of the summary dataframe

Code
df_summary_mimic = dfs_mimic.get("summary")
# df_summary_mimic.columns

2.1 Summary of All Metrics

This table provides a comprehensive overview of all the metrics calculated for each classifier during the cross-validation process.

Code
df_summary_mimic
Table 1: Comprehensive summary of performance metrics for all classifiers.
shape: (8, 40)
classifier cv_folds n_samples n_features n_classes fit_time_mean fit_time_std score_time_mean score_time_std eer_mean auc_macro accuracy_test_mean accuracy_test_std accuracy_train_mean accuracy_train_std precision_macro_test_mean precision_macro_test_std precision_macro_train_mean precision_macro_train_std recall_macro_test_mean recall_macro_test_std recall_macro_train_mean recall_macro_train_std f1_macro_test_mean f1_macro_test_std f1_macro_train_mean f1_macro_train_std precision_weighted_test_mean precision_weighted_test_std precision_weighted_train_mean precision_weighted_train_std recall_weighted_test_mean recall_weighted_test_std recall_weighted_train_mean recall_weighted_train_std f1_weighted_test_mean f1_weighted_test_std f1_weighted_train_mean f1_weighted_train_std overfitting_f1_macro
str i64 i64 i64 i64 f64 f64 f64 f64 f64 f64 f64 f64 f64 f64 f64 f64 f64 f64 f64 f64 f64 f64 f64 f64 f64 f64 f64 f64 f64 f64 f64 f64 f64 f64 f64 f64 f64 f64 f64
"Random Forest" 5 560 52 14 0.588699 0.015079 0.022865 0.000546 0.000206 0.999988 0.994643 0.004374 1.0 0.0 0.995238 0.003888 1.0 0.0 0.994643 0.004374 1.0 0.0 0.994622 0.004391 1.0 0.0 0.995238 0.003888 1.0 0.0 0.994643 0.004374 1.0 0.0 0.994622 0.004391 1.0 0.0 0.005378
"Logistic Regression" 5 560 52 14 0.572919 0.01563 0.014088 0.000784 0.025 0.991484 0.9375 0.027082 0.980357 0.003571 0.943598 0.023792 0.980645 0.003575 0.9375 0.027082 0.980357 0.003571 0.937453 0.025935 0.980358 0.00354 0.943598 0.023792 0.980645 0.003575 0.9375 0.027082 0.980357 0.003571 0.937453 0.025935 0.980358 0.00354 0.042906
"KNN" 5 560 52 14 0.001035 0.000118 0.05246 0.007375 0.09581 0.927539 0.653571 0.034993 0.783036 0.00554 0.661246 0.057675 0.795316 0.007389 0.653571 0.034993 0.783036 0.00554 0.636363 0.037546 0.77969 0.005091 0.661246 0.057675 0.795316 0.007389 0.653571 0.034993 0.783036 0.00554 0.636363 0.037546 0.77969 0.005091 0.143327
"Decision Tree" 5 560 52 14 0.02455 0.003869 0.016146 0.003409 0.013462 0.986538 0.975 0.010412 1.0 0.0 0.977897 0.00917 1.0 0.0 0.975 0.010412 1.0 0.0 0.97473 0.010631 1.0 0.0 0.977897 0.00917 1.0 0.0 0.975 0.010412 1.0 0.0 0.97473 0.010631 1.0 0.0 0.02527
"Naive Bayes" 5 560 52 14 0.002473 0.000366 0.014152 0.00107 0.002266 0.998769 0.982143 0.005647 0.999107 0.001094 0.984762 0.004556 0.999134 0.00106 0.982143 0.005647 0.999107 0.001094 0.98226 0.005524 0.999107 0.001094 0.984762 0.004556 0.999134 0.00106 0.982143 0.005647 0.999107 0.001094 0.98226 0.005524 0.999107 0.001094 0.016847
"Gradient Boosting" 5 560 52 14 16.86945 0.036797 0.016299 0.00071 0.014766 0.998712 0.964286 0.024614 1.0 0.0 0.967221 0.022527 1.0 0.0 0.964286 0.024614 1.0 0.0 0.964086 0.024738 1.0 0.0 0.967221 0.022527 1.0 0.0 0.964286 0.024614 1.0 0.0 0.964086 0.024738 1.0 0.0 0.035914
"Extra Trees" 5 560 52 14 0.258135 0.054932 0.025242 0.00295 0.0 1.0 0.998214 0.003571 1.0 0.0 0.998413 0.003175 1.0 0.0 0.998214 0.003571 1.0 0.0 0.998207 0.003585 1.0 0.0 0.998413 0.003175 1.0 0.0 0.998214 0.003571 1.0 0.0 0.998207 0.003585 1.0 0.0 0.001793
"LDA" 5 560 52 14 0.005623 0.000644 0.016391 0.003201 0.001374 0.99825 0.983929 0.008748 1.0 0.0 0.985317 0.008286 1.0 0.0 0.983929 0.008748 1.0 0.0 0.98388 0.008765 1.0 0.0 0.985317 0.008286 1.0 0.0 0.983929 0.008748 1.0 0.0 0.98388 0.008765 1.0 0.0 0.01612

Display the table as latex code

Code
dataframe_to_latex_htmltab(
    "ejemplo",
    df_summary_mimic,
    [
        "classifier",
        # "cv_folds",
        # "n_samples",
        # "n_features",
        # "n_classes",
        "fit_time_mean",
        # "fit_time_std",
        "score_time_mean",
        # "score_time_std",
        "eer_mean",
        "auc_macro",
        "accuracy_test_mean",
        # "accuracy_test_std",
        # "accuracy_train_mean",
        # "accuracy_train_std",
        "precision_macro_test_mean",
        # "precision_macro_test_std",
        # "precision_macro_train_mean",
        # "precision_macro_train_std",
        "recall_macro_test_mean",
        # "recall_macro_test_std",
        # "recall_macro_train_mean",
        # "recall_macro_train_std",
        "f1_macro_test_mean",
        # "f1_macro_test_std",
        # "f1_macro_train_mean",
        # "f1_macro_train_std",
        # "precision_weighted_test_mean",
        # "precision_weighted_test_std",
        # "precision_weighted_train_mean",
        # "precision_weighted_train_std",
        # "recall_weighted_test_mean",
        # "recall_weighted_test_std",
        # "recall_weighted_train_mean",
        # "recall_weighted_train_std",
        # "f1_weighted_test_mean",
        # "f1_weighted_test_std",
        # "f1_weighted_train_mean",
        # "f1_weighted_train_std",
        "overfitting_f1_macro",
    ],
)
\begin{table}[H]
    \centering
    \caption{Tabla autogenerada}
    \label{tab:ejemplo}
    \begin{htmltab}
        \begin{thead}
            \HTtr{
                \HTtd{classifier}
                \HTtd{fit\_time\_mean}
                \HTtd{score\_time\_mean}
                \HTtd{eer\_mean}
                \HTtd{auc\_macro}
                \HTtd{accuracy\_test\_mean}
                \HTtd{precision\_macro\_test\_mean}
                \HTtd{recall\_macro\_test\_mean}
                \HTtd{f1\_macro\_test\_mean}
                \HTtd{overfitting\_f1\_macro}
            }
        \end{thead}
        \begin{tbody}
            \HTtr{
                \HTtd{Random Forest}
                \HTtd{0.588699197769165}
                \HTtd{0.022864866256713866}
                \HTtd{0.00020604395604395604}
                \HTtd{0.9999879807692308}
                \HTtd{0.9946428571428572}
                \HTtd{0.9952380952380953}
                \HTtd{0.9946428571428572}
                \HTtd{0.9946218487394958}
                \HTtd{0.005378151260504227}
            }
            \HTtr{
                \HTtd{Logistic Regression}
                \HTtd{0.5729194164276123}
                \HTtd{0.014087820053100586}
                \HTtd{0.025000000000000012}
                \HTtd{0.9914835164835163}
                \HTtd{0.9375}
                \HTtd{0.9435977118119976}
                \HTtd{0.9375}
                \HTtd{0.9374527530310368}
                \HTtd{0.04290555441743471}
            }
            \HTtr{
                \HTtd{KNN}
                \HTtd{0.0010349750518798828}
                \HTtd{0.052459716796875}
                \HTtd{0.09581043956043957}
                \HTtd{0.9275394917582417}
                \HTtd{0.6535714285714286}
                \HTtd{0.6612459366030794}
                \HTtd{0.6535714285714286}
                \HTtd{0.6363633981380554}
                \HTtd{0.1433268098899232}
            }
            \HTtr{
                \HTtd{Decision Tree}
                \HTtd{0.02455029487609863}
                \HTtd{0.016146087646484376}
                \HTtd{0.013461538461538467}
                \HTtd{0.9865384615384617}
                \HTtd{0.975}
                \HTtd{0.9778968253968255}
                \HTtd{0.975}
                \HTtd{0.9747302254235027}
                \HTtd{0.02526977457649726}
            }
            \HTtr{
                \HTtd{Naive Bayes}
                \HTtd{0.002472686767578125}
                \HTtd{0.014152097702026366}
                \HTtd{0.0022664835164835184}
                \HTtd{0.9987688873626374}
                \HTtd{0.9821428571428571}
                \HTtd{0.9847619047619048}
                \HTtd{0.9821428571428571}
                \HTtd{0.9822595704948647}
                \HTtd{0.016847354326345987}
            }
            \HTtr{
                \HTtd{Gradient Boosting}
                \HTtd{16.869450330734253}
                \HTtd{0.01629915237426758}
                \HTtd{0.014766483516483525}
                \HTtd{0.9987122252747253}
                \HTtd{0.9642857142857144}
                \HTtd{0.9672206761492476}
                \HTtd{0.9642857142857144}
                \HTtd{0.9640858975169015}
                \HTtd{0.03591410248309845}
            }
            \HTtr{
                \HTtd{Extra Trees}
                \HTtd{0.2581350326538086}
                \HTtd{0.02524228096008301}
                \HTtd{0.0}
                \HTtd{1.0}
                \HTtd{0.9982142857142857}
                \HTtd{0.9984126984126984}
                \HTtd{0.9982142857142857}
                \HTtd{0.9982072829131653}
                \HTtd{0.0017927170868347053}
            }
            \HTtr{
                \HTtd{LDA}
                \HTtd{0.0056225299835205075}
                \HTtd{0.016390609741210937}
                \HTtd{0.0013736263736263744}
                \HTtd{0.9982503434065934}
                \HTtd{0.9839285714285714}
                \HTtd{0.9853174603174605}
                \HTtd{0.9839285714285714}
                \HTtd{0.9838795518207284}
                \HTtd{0.016120448179271607}
            }
        \end{tbody}
    \end{htmltab}
\end{table}
'\\begin{table}[H]\n    \\centering\n    \\caption{Tabla autogenerada}\n    \\label{tab:ejemplo}\n    \\begin{htmltab}\n        \\begin{thead}\n            \\HTtr{\n                \\HTtd{classifier}\n                \\HTtd{fit\\_time\\_mean}\n                \\HTtd{score\\_time\\_mean}\n                \\HTtd{eer\\_mean}\n                \\HTtd{auc\\_macro}\n                \\HTtd{accuracy\\_test\\_mean}\n                \\HTtd{precision\\_macro\\_test\\_mean}\n                \\HTtd{recall\\_macro\\_test\\_mean}\n                \\HTtd{f1\\_macro\\_test\\_mean}\n                \\HTtd{overfitting\\_f1\\_macro}\n            }\n        \\end{thead}\n        \\begin{tbody}\n            \\HTtr{\n                \\HTtd{Random Forest}\n                \\HTtd{0.588699197769165}\n                \\HTtd{0.022864866256713866}\n                \\HTtd{0.00020604395604395604}\n                \\HTtd{0.9999879807692308}\n                \\HTtd{0.9946428571428572}\n                \\HTtd{0.9952380952380953}\n                \\HTtd{0.9946428571428572}\n                \\HTtd{0.9946218487394958}\n                \\HTtd{0.005378151260504227}\n            }\n            \\HTtr{\n                \\HTtd{Logistic Regression}\n                \\HTtd{0.5729194164276123}\n                \\HTtd{0.014087820053100586}\n                \\HTtd{0.025000000000000012}\n                \\HTtd{0.9914835164835163}\n                \\HTtd{0.9375}\n                \\HTtd{0.9435977118119976}\n                \\HTtd{0.9375}\n                \\HTtd{0.9374527530310368}\n                \\HTtd{0.04290555441743471}\n            }\n            \\HTtr{\n                \\HTtd{KNN}\n                \\HTtd{0.0010349750518798828}\n                \\HTtd{0.052459716796875}\n                \\HTtd{0.09581043956043957}\n                \\HTtd{0.9275394917582417}\n                \\HTtd{0.6535714285714286}\n                \\HTtd{0.6612459366030794}\n                \\HTtd{0.6535714285714286}\n                \\HTtd{0.6363633981380554}\n                \\HTtd{0.1433268098899232}\n            }\n            \\HTtr{\n                \\HTtd{Decision Tree}\n                \\HTtd{0.02455029487609863}\n                \\HTtd{0.016146087646484376}\n                \\HTtd{0.013461538461538467}\n                \\HTtd{0.9865384615384617}\n                \\HTtd{0.975}\n                \\HTtd{0.9778968253968255}\n                \\HTtd{0.975}\n                \\HTtd{0.9747302254235027}\n                \\HTtd{0.02526977457649726}\n            }\n            \\HTtr{\n                \\HTtd{Naive Bayes}\n                \\HTtd{0.002472686767578125}\n                \\HTtd{0.014152097702026366}\n                \\HTtd{0.0022664835164835184}\n                \\HTtd{0.9987688873626374}\n                \\HTtd{0.9821428571428571}\n                \\HTtd{0.9847619047619048}\n                \\HTtd{0.9821428571428571}\n                \\HTtd{0.9822595704948647}\n                \\HTtd{0.016847354326345987}\n            }\n            \\HTtr{\n                \\HTtd{Gradient Boosting}\n                \\HTtd{16.869450330734253}\n                \\HTtd{0.01629915237426758}\n                \\HTtd{0.014766483516483525}\n                \\HTtd{0.9987122252747253}\n                \\HTtd{0.9642857142857144}\n                \\HTtd{0.9672206761492476}\n                \\HTtd{0.9642857142857144}\n                \\HTtd{0.9640858975169015}\n                \\HTtd{0.03591410248309845}\n            }\n            \\HTtr{\n                \\HTtd{Extra Trees}\n                \\HTtd{0.2581350326538086}\n                \\HTtd{0.02524228096008301}\n                \\HTtd{0.0}\n                \\HTtd{1.0}\n                \\HTtd{0.9982142857142857}\n                \\HTtd{0.9984126984126984}\n                \\HTtd{0.9982142857142857}\n                \\HTtd{0.9982072829131653}\n                \\HTtd{0.0017927170868347053}\n            }\n            \\HTtr{\n                \\HTtd{LDA}\n                \\HTtd{0.0056225299835205075}\n                \\HTtd{0.016390609741210937}\n                \\HTtd{0.0013736263736263744}\n                \\HTtd{0.9982503434065934}\n                \\HTtd{0.9839285714285714}\n                \\HTtd{0.9853174603174605}\n                \\HTtd{0.9839285714285714}\n                \\HTtd{0.9838795518207284}\n                \\HTtd{0.016120448179271607}\n            }\n        \\end{tbody}\n    \\end{htmltab}\n\\end{table}'

2.2 Biometric Performance Metrics

Here we evaluate the models using metrics that are standard in the field of biometric identification. These are often the most important indicators of a system’s real-world performance.

2.2.1 Equal Error Rate (EER)

The Equal Error Rate is a key biometric metric. It represents the point where the False Acceptance Rate (FAR) is equal to the False Rejection Rate (FRR). A lower EER indicates a more accurate and reliable system, making it a primary metric for comparing biometric systems.

Code
plot_metric(
    df_summary_mimic,
    "Equal Error Rate (EER)",
    "eer_mean",
    "EER (Lower is Better)",
    image_name=mimic_image_name + "_eer"
)
Figure 1: Comparison of Equal Error Rate (EER) across classifiers. Lower is better.

2.2.2 Area Under ROC Curve (AUC)

The Area Under the Receiver Operating Characteristic (ROC) Curve provides an aggregate measure of performance across all possible classification thresholds. An AUC of 1.0 represents a perfect model, while 0.5 represents a model with no discriminative power. A higher AUC is better. We use the macro-averaged AUC, which is suitable for multiclass problems.

Code
plot_metric(
    df_summary_mimic,
    "Area Under ROC Curve (AUC)",
    "auc_macro",
    "AUC (Higher is Better)",
    image_name=mimic_image_name + "_auc"
)
Figure 2: Comparison of Area Under ROC Curve (AUC) across classifiers. Higher is better.

2.3 Computational Performance

This section visualizes the computational performance of each model. We analyze both the time required to train the model and the time it takes to make a prediction on new data.

2.3.1 Training Time

This plot shows the average time (in seconds) each classifier took to train on a single fold of the cross-validation. It helps to understand the computational cost of building each model.

Code
plot_metric(
    df_summary_mimic,
    "Training Time",
    "fit_time_mean",
    "Fit Time (s)",
    image_name=mimic_image_name + "_training_time"
)
Figure 3: Average training time per fold for each classifier.

2.3.2 Prediction Time

This plot shows the average time (in seconds) each trained classifier took to make predictions on the test data from a single fold. This metric is critical for evaluating the model’s suitability for real-time or large-scale applications.

Code
plot_metric(
    df_summary_mimic,
    "Prediction Time",
    "score_time_mean",
    "Prediction Time (s)",
    image_name=mimic_image_name + "_prediction_time"
)
Figure 4: Average prediction time per fold for each classifier.

2.4 Standard Classification Metrics

While biometric-specific metrics are crucial, we also analyze standard classification metrics to get a comprehensive view of model behavior.

2.4.1 Accuracy

Accuracy measures the proportion of correctly classified instances. While simple to understand, it can be misleading in datasets with imbalanced classes.

Code
plot_metric(df_summary_mimic, "Accuracy test", "accuracy_test_mean", "Accuracy test (mean)", image_name=mimic_image_name + "_accuracy_test")
Figure 5: Test accuracy for each classifier.

2.4.2 F1-Score (Macro)

The F1-Score is the harmonic mean of precision and recall. The macro-averaged F1-score calculates the metric independently for each class and then takes the average, treating all classes equally. It’s a robust metric for imbalanced datasets.

Code
plot_metric(df_summary_mimic, "F1 Macro test", "f1_macro_test_mean", "F1 macro test (mean)", image_name=mimic_image_name + "_f1_macro_test")
Figure 6: Macro F1-Score on test data for each classifier.

2.5 Model Behavior Analysis

2.5.1 Overfitting Analysis

This plot shows the difference between the training F1-score and the test F1-score. A large positive value suggests that the model is overfitting: it performs well on the data it has seen but fails to generalize to new, unseen data. A value close to zero is ideal.

Code
plot_metric(df_summary_mimic, "Overfitting F1 Macro", "overfitting_f1_macro", "Overfitting f1 macro", image_name=mimic_image_name + "_overfitting_f1_macro")
Figure 7: Overfitting analysis based on the F1-score difference between train and test sets.

2.6 Detailed Biometric Analysis

Here we dive deeper into the biometric performance, first with a global overview and then by inspecting the top-performing models individually.

2.6.1 Global Performance Heatmaps

Heatmaps provide a dense, visual summary of performance, allowing for the quick identification of patterns across models and subjects.

EER Heatmap

This heatmap visualizes the Equal Error Rate (EER) for each classifier against each subject. Greener cells indicate better performance (lower EER), helping us spot if certain subjects are difficult for all models or if certain models fail on specific subjects.

Code
plot_biometric_heatmap(mimic_reporter, metric='eer', image_name=mimic_image_name + "_eer_heatmap")
Figure 8: Heatmap of EER per classifier and subject. Green indicates better performance.

AUC Heatmap

This heatmap shows the Area Under the ROC Curve (AUC) for each classifier and subject. Greener cells represent superior performance (higher AUC).

Code
plot_biometric_heatmap(mimic_reporter, metric='auc', image_name=mimic_image_name + "_auc_heatmap")
Figure 9: Heatmap of AUC per classifier and subject. Green indicates better performance.

2.6.2 In-Depth Look at Top Classifiers

Based on the global analysis, we now delve into a detailed examination of the top-performing classifiers based on the mean EER. For each model, we analyze its per-class EER and its FAR/FRR curves.

Code
  for classifier in top_3_classifiers:
    plot_biometrics_per_class(mimic_reporter, classifier, metric='eer', image_name=f"{mimic_image_name}_{classifier.replace(' ', '_')}_eer_per_class")
    plot_far_frr_curves(mimic_reporter, classifier, image_name=f"{mimic_image_name}_{classifier.replace(' ', '_')}_det_curve")

2.7 Confusion Matrix

The confusion matrix provides a detailed breakdown of classification results. Each row represents the instances in an actual class, while each column represents the instances in a predicted class. This allows us to see not only the errors but also the types of errors being made (e.g., which classes are most often confused with each other).